#!/usr/bin/env node

const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const { PerformanceObserver, performance } = require('perf_hooks');

const better_sqlite3 = require('better-sqlite3');
const chokidar = require('chokidar');
// This library is terrible, too much magic, hard to understand interface,
// does not do some obvious basics.
const commander = require('commander');
const git_url_parse = require("git-url-parse");
const is_installed_globally = require('is-installed-globally');
const sass = require('sass');

const cirodown = require('cirodown');
const cirodown_nodejs = require('cirodown/nodejs');

const CIRODOWN_JSON_BASENAME = 'cirodown.json';
const LOG_OPTIONS = new Set([
  'ast',
  'headers',
  'tokens',
]);
const SASS_EXT = '.scss';
const TMP_DIRNAME = 'out';
const DEFAULT_IGNORE_BASENAMES = [
  '.git',
  '.gitignore',
  '.sass-cache',
  'main.liquid.html',
  'node_modules',
  'package-lock.json',
  'package.json',
  TMP_DIRNAME,
  cirodown_nodejs.PACKAGE_SASS_BASENAME,
];
const DEFAULT_IGNORE_BASENAMES_SET = new Set(DEFAULT_IGNORE_BASENAMES);

class IdProviderDbAdapter {
  constructor(convert_input_options) {
  }
}

class SqliteIdProvider extends cirodown.IdProvider {
  constructor(convert_input_options) {
    super();
    this.delete_ids_stmt = convert_input_options.db.prepare(
      `DELETE FROM '${convert_input_options.ids_table_name}' WHERE path = ?`);
    this.delete_includes_stmt = convert_input_options.db.prepare(
      `DELETE FROM '${convert_input_options.includes_table_name}' WHERE from_path = ?`);
    this.get_id_stmt = convert_input_options.db.prepare(
      `SELECT path,ast_json FROM ${convert_input_options.ids_table_name} WHERE id = ?`);
    this.get_includes_stmt = convert_input_options.db.prepare(
      `SELECT from_id FROM ${convert_input_options.includes_table_name} WHERE to_id = ?`);
  }

  clear(input_path) {
    this.delete_ids_stmt.run(input_path);
    this.delete_includes_stmt.run(input_path);
  }

  get_noscope_entry(id) {
    return this.get_id_stmt.get(id);
  }

  get_includes_entries(to_id) {
    return this.get_includes_stmt.all(to_id);
  }
}

class SqliteFileProvider extends cirodown.FileProvider {
  constructor(convert_input_options) {
    super();
    this.get_file_stmt = convert_input_options.db.prepare(
      `SELECT * FROM ${convert_input_options.files_table_name} WHERE path = ?`);
    this.get_file_by_id_stmt = convert_input_options.db.prepare(
      `SELECT * FROM ${convert_input_options.files_table_name} WHERE toplevel_id = ?`);
  }

  get_id(id) {
    return this.get_file_by_id_stmt.get(id);
  }

  get_path_entry(path) {
    return this.get_file_stmt.get(path);
  }
}

/** Report an error with the CLI usage and exit in error. */
function cli_error(message) {
  console.error(`error: ${message}`);
  process.exit(1);
}

function cmd_get_stdout(cmd, args=[], options={}) {
  if (!('dry_run' in options)) {
    options.dry_run = false;
  }
  if (!('show_cmd' in options)) {
    options.show_cmd = true;
  }
  let out;
  const cmd_str = ([cmd].concat(args)).join(' ');
  if (options.show_cmd) {
    console.log(cmd_str);
  }
  if (!options.dry_run) {
    out = child_process.spawnSync(cmd, args);
  }
  let ret;
  if (options.dry_run) {
    ret = '';
  } else {
    if (out.status != 0 && options.throw_on_error) {
      let msg = `cmd: \n${cmd_str}\n`;
      if (out.stdout !== null) {
        msg += `stdout: \n${out.stdout.toString(cirodown_nodejs.ENCODING)}\n`;
      }
      if (out.stderr !== null) {
        msg += `stdout: \n${out.stdout.toString(cirodown_nodejs.ENCODING)}\n`;
      }
      throw msg;
    }
    ret = out.stdout.toString(cirodown_nodejs.ENCODING);
  }
  return ret;
}

/**
 * @param {String} path to a directory to convert files in
 */
function convert_files_in_directory(input_path, options, convert_input_options) {
  for (const one_path of walk_files_recursively(input_path, DEFAULT_IGNORE_BASENAMES_SET)) {
    convert_path_to_file(input_path, path.relative(input_path, one_path), options, convert_input_options);
  }
}

/** Extract IDs from all input files into the ID database, without fully converting. */
function convert_files_in_directory_extract_ids(input_path, options, convert_input_options) {
  convert_files_in_directory(
    input_path,
    cirodown.clone_and_set(options, 'render', false),
    convert_input_options
  );
}

/** Convert input from a string to output and return the output as a string.
 *
 * Wraps cirodown.convert with CLI usage convenience.
 *
 * @param {String} input
 * @param {Object} options - options to be passed to cirodown.convert
 * @param {Object} convert_input_options - control options for this function,
 *                 not passed to cirodown.convert. Also contains some returns:
 *                 - {bool} had_error
 *                 - {Object} extra_returns
 * @return {String}
 */
function convert_input(input, options, convert_input_options={}) {
  const new_options = Object.assign({}, options);
  if ('input_path' in convert_input_options) {
    new_options.input_path = convert_input_options.input_path;
  }
  if ('db' in convert_input_options) {
    new_options.id_provider = convert_input_options.id_provider;
    new_options.file_provider = convert_input_options.file_provider;
  }
  if ('title' in convert_input_options) {
    new_options.title = convert_input_options.title;
  }
  new_options.extra_returns = {};
  // If we don't where the output will go (the case for stdout) or
  // the user did not explicitly request full embedding, inline all CSS.
  // Otherwise, include and external CSS to make each page lighter.
  if (convert_input_options.commander.htmlEmbed) {
    new_options.template_vars.style = fs.readFileSync(
      cirodown_nodejs.PACKAGE_OUT_CSS_EMBED_PATH,
      cirodown_nodejs.ENCODING
    );
    let scripts_str = '';
    for (const script_path of cirodown_nodejs.JS_LOCAL_INCLUDES) {
      scripts_str += `<script>${fs.readFileSync(script_path)}</script>\n`
    }
    scripts_str += `<script>${fs.readFileSync(
      cirodown_nodejs.PACKAGE_OUT_JS_LOCAL_PATH, cirodown_nodejs.ENCODING)}</script>\n`;
    new_options.template_vars.post_body = scripts_str;
  } else {
    let includes_str = ``;
    let scripts_str = ``;

    let includes = [];
    let scripts = [];

    if (convert_input_options.external_css_and_js) {
      scripts.push(
        'https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/tablesort.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/sorts/tablesort.date.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/sorts/tablesort.dotsep.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/sorts/tablesort.filesize.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/sorts/tablesort.monthname.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/sorts/tablesort.number.min.js',
      );
    }

    let includes_local = [];
    let scripts_local = [];

    if (!convert_input_options.external_css_and_js) {
      includes_local.push(...cirodown_nodejs.CSS_LOCAL_INCLUDES);
      scripts_local.push(...cirodown_nodejs.JS_LOCAL_INCLUDES);
    }

    includes_local.push(convert_input_options.out_css_path);
    scripts_local.push(convert_input_options.out_js_path);

    if (
      options.outfile !== undefined &&
      !is_installed_globally
    ) {
      for (const include of includes_local) {
        includes.push(path.relative(path.dirname(options.outfile), include));
      }
      for (const script of scripts_local) {
        scripts.push(path.relative(path.dirname(options.outfile), script));
      }
    } else {
      includes.push(...includes_local);
      scripts.push(...scripts_local);
    }

    for (const include of includes) {
      includes_str += `@import "${include}";\n`;
    }
    for (const script of scripts) {
      scripts_str += `<script src="${script}"></script>\n`;
    }

    new_options.template_vars.style = `\n${includes_str}`;
    new_options.template_vars.post_body = `${scripts_str}`;
  }
  // Finally, do the conversion!
  const output = cirodown.convert(input, new_options, new_options.extra_returns);
  for (const id in new_options.extra_returns.rendered_outputs) {
    const output_path = path.join(convert_input_options.outdir, id);
    fs.mkdirSync(path.dirname(output_path), {recursive: true});
    fs.writeFileSync(output_path, new_options.extra_returns.rendered_outputs[id]);
  }
  if (convert_input_options.log.tokens) {
    console.error('tokens:');
    console.error(JSON.stringify(new_options.extra_returns.tokens, null, 2));
    console.error();
  }
  if (convert_input_options.log.ast) {
    console.error('ast:');
    console.error(JSON.stringify(new_options.extra_returns.ast, null, 2));
    console.error();
  }
  if (convert_input_options.commander.debugPerf) {
    console.error(`perf start: ${new_options.extra_returns.debug_perf.start}`);
    console.error(`perf tokenize_pre: ${new_options.extra_returns.debug_perf.tokenize_pre}`);
    console.error(`perf tokenize_post: ${new_options.extra_returns.debug_perf.tokenize_post}`);
    console.error(`perf parse_start: ${new_options.extra_returns.debug_perf.parse_start}`);
    console.error(`perf post_process_start: ${new_options.extra_returns.debug_perf.post_process_start}`);
    console.error(`perf post_process_end: ${new_options.extra_returns.debug_perf.post_process_end}`);
    console.error(`perf render_pre: ${new_options.extra_returns.debug_perf.render_pre}`);
    console.error(`perf render_post: ${new_options.extra_returns.debug_perf.render_post}`);
    console.error(`perf end: ${new_options.extra_returns.debug_perf.end}`);
  }
  for (const error of new_options.extra_returns.errors) {
    console.error(error.toString());
  }
  convert_input_options.extra_returns = new_options.extra_returns;
  if (new_options.extra_returns.errors.length > 0) {
    convert_input_options.had_error = true;
  }
  if (convert_input_options.commander.debugPerf) {
    console.error(`perf convert_input_end ${performance.now()}`);
  }
  return output;
}

/** Convert cirodown input from a path to string output and return the output as a string.
 *
 * @return {String}
 */
function convert_path(input_path, options, convert_input_options) {
  let full_path = path.resolve(input_path);
  let new_options = Object.assign({}, options);
  let new_convert_input_options = Object.assign({}, convert_input_options);
  let input = fs.readFileSync(full_path, new_convert_input_options.encoding);
  let input_path_parse = path.parse(full_path);
  let input_path_basename_noext = input_path_parse.name;
  let path_relative_to_cirodown_json;
  if (options.cirodown_json_dir !== undefined) {
    path_relative_to_cirodown_json = path.relative(options.cirodown_json_dir, input_path_parse.dir);
  }
  let toplevel_id;
  if (cirodown.INDEX_FILE_BASENAMES_NOEXT.has(input_path_basename_noext)) {
    if (path_relative_to_cirodown_json === '') {
      // https://cirosantilli.com/cirodown#the-toplevel-index-file
      toplevel_id = undefined;
    } else {
      // https://cirosantilli.com/cirodown#the-id-of-the-first-header-is-derived-from-the-filename
      toplevel_id = path_relative_to_cirodown_json;
      new_options.toplevel_has_scope = true;
    }
  } else {
    toplevel_id = input_path_basename_noext;
    if (path_relative_to_cirodown_json === '') {
      new_options.toplevel_parent_scope = undefined;
    } else {
      new_options.toplevel_parent_scope = path_relative_to_cirodown_json;
    }
  }
  if (!convert_input_options.commander.htmlEmbed) {
    let root_relpath = path.relative(input_path_parse.dir, options.cirodown_json_dir)
    if (root_relpath !== '') {
      root_relpath += cirodown.URL_SEP;
    }
    new_options.template_vars.root_relpath = root_relpath;
  }
  new_options.toplevel_id = toplevel_id;
  new_convert_input_options.input_path = input_path;
  let output = convert_input(input, new_options, new_convert_input_options);
  const context = new_convert_input_options.extra_returns.context;
  if (convert_input_options.log.headers) {
    console.error(context.header_graph.toString());
  }

  // Update the Sqlite databse with results from the conversion.
  if (convert_input_options.commander.debugPerf) {
    console.error(`perf convert_path_pre_sqlite ${performance.now()}`);
  }
  if ('db' in convert_input_options) {
    const ids = new_convert_input_options.extra_returns.ids;
    const insert_id_stmt = convert_input_options.db.prepare(
      `INSERT INTO '${convert_input_options.ids_table_name}'
      (id, path, ast_json) VALUES (?, ?, ?);`
    );
    const insert_include_stmt = convert_input_options.db.prepare(
      `INSERT INTO '${convert_input_options.includes_table_name}'
      (from_id, from_path, to_id) VALUES (?, ?, ?);`
    );
    const insert_file_stmt = convert_input_options.db.prepare(
      `INSERT OR REPLACE INTO '${convert_input_options.files_table_name}' ` +
      `(path, toplevel_id) VALUES (?, ?);`
    );
    if (convert_input_options.commander.debugPerf) {
      console.error(`perf convert_path_pre_sqlite_transaction ${performance.now()}`);
    }
    // This was the 80% bottleneck at Cirodown f8fc9eacfa794b95c1d9982a04b62603e6d0bb83
    // before being converted to a single transaction!
    convert_input_options.db.transaction(() => {
      for (const id in ids) {
        const ast = ids[id];
        insert_id_stmt.run(id, ast.source_location.path, JSON.stringify(ast));
      }
      for (const header_ast of context.headers_with_include) {
        for (const include of header_ast.includes) {
          insert_include_stmt.run(header_ast.id, header_ast.source_location.path, include);
        }
      }
      let toplevel_id;
      if (context.toplevel_ast !== undefined) {
        toplevel_id = context.toplevel_ast.id;
      }
      insert_file_stmt.run(
        path.join(path_relative_to_cirodown_json, input_path_parse.base),
        toplevel_id
      );
    })();
    if (convert_input_options.commander.debugPerf) {
      console.error(`perf convert_path_post_sqlite_transaction ${performance.now()}`);
    }
  }

  if (new_convert_input_options.had_error) {
    convert_input_options.had_error = true;
  }
  if (convert_input_options.commander.debugPerf) {
    console.error(`perf convert_path_end ${performance.now()}`);
  }
  return output;
}

/** Convert filetypes that cirodown knows how to convert, and just copy those that we don't, e.g.:
 *
 * * .ciro to .html
 * * .scss to .css
 *
 * @param {string} input_path - path relative to the base_path, e.g. `./cirodown subdir` gives:
 *   base_path: "subdir" and input_path "index.ciro" amongst other files.
 *
 * The output file name is derived from the input file name with the output extension.
 */
function convert_path_to_file(base_path, input_path, options, convert_input_options={}) {
  let input_path_parse = path.parse(input_path);
  if (input_path_parse.ext === cirodown.CIRODOWN_EXT) {
    let message_prefix;
    if (options.render) {
      message_prefix = 'convert';
    } else {
      message_prefix = 'extract_ids';
    }
    let message = `${message_prefix} ${input_path}`;
    console.log(message);
    const new_convert_input_options = Object.assign({}, convert_input_options);
    let t0 = performance.now();
    convert_path(path.join(base_path, input_path), options, new_convert_input_options);
    let t1 = performance.now();
    console.log(`${message} finished in ${t1 - t0} ms`);
    if (new_convert_input_options.had_error) {
      convert_input_options.had_error = true;
    }
  } else {
    let output_name = input_path_parse.name;
    let output_path_noext = path.join(input_path_parse.dir, output_name);
    if (options.outfile === undefined) {
      output_path_noext = path.join(convert_input_options.outdir, output_path_noext);
    } else {
      output_path_noext = options.outfile;
    }
    fs.mkdirSync(path.dirname(output_path_noext), {recursive: true});
    if (options.render) {
      if (input_path_parse.ext === SASS_EXT) {
        console.log(`sass ${input_path}`);
        fs.writeFileSync(
          output_path_noext + '.css',
          sass.renderSync({
            data: fs.readFileSync(input_path, convert_input_options.encoding),
            outputStyle: 'compressed',
          }).css
        );
      } else {
        // Otherwise, just copy the file over if needed.
        const output_path = output_path_noext + input_path_parse.ext;
        if (output_path !== input_path) {
          console.log(`copy ${input_path}`);
          fs.copyFileSync(input_path, output_path);
        }
      }
    }
  }
  if (convert_input_options.commander.debugPerf) {
    console.error(`perf convert_path_to_file_end ${performance.now()}`);
  }
}

function generate_redirects(
  options,
  cirodown_json,
  outdir
) {
  for (let redirect_src_id in cirodown_json.redirects) {
    const redirect_target_id = cirodown_json.redirects[redirect_src_id];
    options = Object.assign({}, options);
    options.input_path = redirect_src_id;
    const redirect_href = cirodown.convert_x_href(redirect_target_id, options);
    if (redirect_href === undefined) {
      cli_error(`redirection target ID "${redirect_target_id}" not found`);
    }
    // https://stackoverflow.com/questions/10178304/what-is-the-best-approach-for-redirection-of-old-pages-in-jekyll-and-github-page/36848440#36848440
    fs.writeFileSync(path.join(outdir, redirect_src_id + '.' + cirodown.HTML_EXT),
`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<link rel="canonical" href="${redirect_href}"/>
<meta http-equiv="refresh" content="0;url=${redirect_href}" />
</head>
<body>
<h1>Redirecting...</h1>
<a href="${redirect_href}">Click here if you are not redirected.<a>
<script>location='${redirect_href}'</script>
</body>
</html>
`);
  }
}

/**
 * @return {String} full Git SHA of the source.
 */
function git_sha(input_path, src_branch) {
  const args = ['-C', input_path, 'log', '-n1', '--pretty=%H'];
  if (src_branch !== undefined) {
    args.push(src_branch);
  }
  return cmd_get_stdout('git', args, {show_cmd: false, throw_on_error: true}).slice(0, -1);
}

/** https://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search
 *
 * @param {Set} skip_basenames
 */
function* walk_files_recursively(file_or_dir, skip_basenames) {
  if (fs.lstatSync(file_or_dir).isDirectory()) {
    const dirents = fs.readdirSync(file_or_dir, {withFileTypes: true});
    for (const dirent of dirents) {
      if (!skip_basenames.has(dirent.name)) {
        const res = path.join(file_or_dir, dirent.name);
        if (dirent.isDirectory()) {
          yield* walk_files_recursively(res, skip_basenames);
        } else {
          yield res;
        }
      }
    }
  } else {
    yield file_or_dir;
  }
}

// CLI options.
commander.option('--body-only', 'output only the content inside the HTLM body element', false);
commander.option('--debug-perf', 'https://cirosantilli.com/linux-kernel-module-cheat#debug-perf', false);
commander.option('--dry-run', "don't run most external commands, see: https://github.com/cirosantilli/linux-kernel-module-cheat/tree/6d0a900f4c3c15e65d850f9d29d63315a6f976bf#dry-run-to-get-commands-for-your-project", false);
commander.option('--embed-includes', 'http://cirosantilli.com/cirodown#embed-include', false);
commander.option('--embed-resources', 'http://cirosantilli.com/cirodown#embed-resources', false);
commander.option('--generate', 'http://cirosantilli.com/cirodown#generate', false);
commander.option('--generate-redirects', 'http://cirosantilli.com/cirodown#redirects', false);
commander.option('--generate-multifile', 'http://cirosantilli.com/cirodown#generate-multifile', false);
commander.option('--help-macros', 'print the metadata of all macros to stdout in JSON format. https://cirosantilli.com/cirodown/#', false);
commander.option('-l, --log <log...>', 'http://cirosantilli.com/cirodown#log');
commander.option('--no-html-x-extension', 'http://cirosantilli.com/cirodown#no-html-x-extension');
commander.option('--no-db', 'ignore the ID database, mostly for testing https://cirosantilli.com/cirosantilli#internal-cross-file-references-internals');
commander.option('--outdir <outdir>', 'if the output would be saved to a file e.g. when building a directory, use this directory as the root');
commander.option('-o, --outfile <outfile>', 'http://cirosantilli.com/cirodown#outfile');
commander.option('-O, --output-format <output-format>', 'https://cirosantilli.com/cirodown#output-format', 'html');
commander.option('-p --publish', 'http://cirosantilli.com/cirodown#publish', false);
commander.option('-P, --publish-commit <commit-message>', 'http://cirosantilli.com/cirodown#publish-commit');
commander.option('--split-headers', 'http://cirosantilli.com/cirodown#split-headers', false);
commander.option('--stdout', 'print output to stdout instead of saving to a file', false);
commander.option('--template <template>', 'http://cirosantilli.com/cirodown#template');
commander.option('-w, --watch', 'http://cirosantilli.com/cirodown#watch', false);
commander.option('--xss-unsafe', 'https://cirosantilli.com/cirodown#xss-unsafe');
let inputPath;
commander.arguments(
  '[input_path]',
  undefined,
  'http://cirosantilli.com/cirodown#cirodown-executable',
).action(function (input_path) {
  inputPath = input_path;
});
commander.parse(process.argv);

// Action.
if (commander.helpMacros) {
  console.log(JSON.stringify(cirodown.macro_list(), null, 2));
} else {
  let input;
  let title;
  let output;
  let publish = commander.publish || commander.publishCommit !== undefined;
  let html_x_extension;
  let cirodown_json = {};
  let input_dir;
  if (inputPath === undefined) {
    if (publish || commander.watch) {
      inputPath = '.';
    }
  }

  // Determine the cirodown.json file by walking up the directory tree.
  let cirodown_json_dir;
  if (inputPath !== undefined) {
    let curdir = path.resolve(inputPath);
    let initial_dir;
    if (fs.lstatSync(inputPath).isFile()) {
      curdir = path.dirname(curdir)
    }
    initial_dir = curdir;
    while (true) {
      const cirodown_json_path = path.join(curdir, CIRODOWN_JSON_BASENAME);
      if (fs.existsSync(cirodown_json_path)) {
        Object.assign(cirodown_json, JSON.parse(fs.readFileSync(cirodown_json_path)));
        cirodown_json_dir = curdir;
        break;
      }
      if (curdir === '/') {
        break;
      }
      curdir = path.dirname(curdir)
    }
    if (cirodown_json_dir === undefined) {
      // No cirodown.json found. So just fake one based on the input path,
      // to resolve scopes in subdirectories.
      cirodown_json_dir = initial_dir;
    }
  }
  if (publish) {
    // GitHub pages target is the only one for now.
    html_x_extension = false;
  } else {
    html_x_extension = commander.htmlXExtension;
  }

  let options = {
    body_only: commander.bodyOnly,
    cirodown_json: cirodown_json,
    cirodown_json_dir: cirodown_json_dir,
    embed_includes: commander.embedIncludes,
    embed_resources: commander.embedResources,
    html_x_extension: html_x_extension,
    output_format: commander.outputFormat,
    outfile: commander.outfile,
    path_sep: path.sep,
    read_include: function(id) {
      let found = undefined;
      let test = id + cirodown.CIRODOWN_EXT;
      if (fs.existsSync(test)) {
        found = test;
      }
      test = path.join(id, cirodown.INDEX_BASENAME_NOEXT + cirodown.CIRODOWN_EXT);
      if (found === undefined) {
        if (fs.existsSync(test)) {
          found = test;
        }
        if (found === undefined) {
          const id_parse = path.parse(id);
          if (id_parse.name === cirodown.INDEX_BASENAME_NOEXT) {
            for (let index_basename_noext of cirodown.INDEX_FILE_BASENAMES_NOEXT) {
              test = path.join(id_parse.dir, index_basename_noext + cirodown.CIRODOWN_EXT);
              if (fs.existsSync(test)) {
                found = test;
                break;
              }
            }
          }
        }
      }
      if (found !== undefined) {
        return [found, fs.readFileSync(found, cirodown_nodejs.ENCODING)];
      };
      return undefined;
    },
    render: true,
    split_headers: commander.splitHeaders,
    template_vars: {},
    xss_unsafe: commander.xssUnsafe,
  };
  options.log = {};
  let convert_input_options_log = {};
  if (commander.log !== undefined) {
    for (const log of commander.log) {
      if (cirodown.LOG_OPTIONS.has(log)) {
        options.log[log] = true;
      } else if (LOG_OPTIONS.has(log)) {
        convert_input_options_log[log] = true;
      } else {
        cli_error('unknown --log option: ' + log);
      }
    }
  }
  if (commander.template !== undefined) {
    options.template = fs.readFileSync(commander.template).toString();
  } else if ('template' in cirodown_json) {
    options.template = fs.readFileSync(cirodown_json.template).toString();
  } else {
    options.template = undefined;
  }
  let input_path_is_file;
  if (inputPath === undefined) {
    // Input from stdin.
    input_dir = undefined;
    input_path_is_file = false;
  } else {
    input_path_is_file = fs.lstatSync(inputPath).isFile();
    if (input_path_is_file) {
      input_dir = path.dirname(inputPath);
    } else {
      input_dir = inputPath;
    }
    try {
      options.template_vars.git_sha = git_sha(input_dir);
    } catch(error) {
      // Not in a git repo.
    }
  }
  let outdir;
  if (commander.outdir === undefined) {
    outdir = '.';
  } else {
    outdir = commander.outdir;
  }
  if (commander.generate || commander.generateMultifile) {
    fs.mkdirSync(outdir, {recursive: true});

    // Generate package.json.
    const package_json = JSON.parse(fs.readFileSync(
      cirodown_nodejs.PACKAGE_PACKAGE_JSON_PATH).toString());
    const package_json_str = `{
  "dependencies": {
    "cirodown": "${package_json.version}"
  }
}
`;
    fs.writeFileSync(path.join(outdir, 'package.json'), package_json_str);

    const new_cirodown_json = {};

    // Generate .gitignore. Reuse our gitignore up to the first blank line.
    let gitignore_new = '';
    const gitignore = fs.readFileSync(
      cirodown_nodejs.GITIGNORE_PATH,
      cirodown_nodejs.ENCODING
    );
    for (const line of gitignore.split('\n')) {
      if (line === '') {
        break;
      }
      gitignore_new += line + '\n';
    }
    fs.writeFileSync(path.join(outdir, '.gitignore'), gitignore_new);

    // Generate README.ciro and other .ciro files.
    let readme_cirodown_str = `= Cirodown hello world

Hello!
`;
    if (commander.generateMultifile) {
      readme_cirodown_str += `
\\Toc

== And now an include

\\Include[not-readme]
`
      not_readme_str = `= Not the readme

A link to another file: \\x[and-now-an-include]
`
      fs.writeFileSync(path.join(outdir, 'not-readme.ciro'), not_readme_str);
    }
    fs.writeFileSync(path.join(outdir, 'README.ciro'), readme_cirodown_str);
    if (commander.generateMultifile) {
      fs.copyFileSync(path.join(cirodown_nodejs.PACKAGE_PATH, 'main.liquid.html'),
        path.join(outdir, 'main.liquid.html'));
      fs.copyFileSync(path.join(cirodown_nodejs.PACKAGE_PATH, 'main.scss'),
        path.join(outdir, 'main.scss'));
      new_cirodown_json.template = 'main.liquid.html';
    }

    if (new_cirodown_json !== {}) {
      fs.writeFileSync(path.join(outdir, CIRODOWN_JSON_BASENAME),
        JSON.stringify(new_cirodown_json, null, 2) + '\n');
    }
    process.exit(0);
  }
  // Options that are not directly passed to cirodown.convert
  // but rather used only by this cirodown executable.
  let convert_input_options = {
    commander: commander,
    encoding: cirodown_nodejs.ENCODING,
    external_css_and_js: false,
    had_error: false,
    log: convert_input_options_log,
    out_css_path: cirodown_nodejs.PACKAGE_OUT_CSS_LOCAL_PATH,
    out_js_path: cirodown_nodejs.PACKAGE_OUT_JS_LOCAL_PATH,
    outdir: outdir,
  };
  if (inputPath === undefined) {
    // Input from stdin.
    title = 'stdin';
    input = fs.readFileSync(0, cirodown_nodejs.ENCODING);
    output = convert_input(input, options, convert_input_options);
  } else {
    if (!fs.existsSync(inputPath)) {
      cli_error(`input_path does not exist: "${inputPath}"`);
    }
    let tmpdir = path.join(cirodown_json_dir, TMP_DIRNAME);
    let publish_dir;
    let publish_out_dir;
    let cmd_options = {
      dry_run: commander.dryRun,
      throw_on_error: true,
    }
    if (input_path_is_file) {
      input_path_is_file = true;
    } else {
      if (commander.outfile !== undefined) {
        cli_error(`--outfile given but multiple output files must be generated, maybe you want --outdir?`);
      }
      input_path_is_file = false;
      if (publish) {
        publish_dir = path.join(tmpdir, 'publish');
        publish_git_dir = path.join(publish_dir, '.git');
        if (fs.existsSync(publish_git_dir)) {
          // This cleanup has to be done before the database initialization.
          cmd_get_stdout('git', ['-C', publish_dir, 'clean', '-x', '-d', '-f'], cmd_options);
        }
        tmpdir = path.join(publish_dir, TMP_DIRNAME);
        publish_out_dir = path.join(publish_dir, TMP_DIRNAME);
      }
    }

    // Setup the ID database.
    fs.mkdirSync(tmpdir, {recursive: true});
    if (commander.db) {
      const db_path = path.join(tmpdir, 'db.sqlite3');
      const ids_table_name = 'ids';
      const includes_table_name = 'includes';
      const files_table_name = 'files';
      const db = new better_sqlite3(db_path);
      if (
        db.prepare(`SELECT name
        FROM sqlite_master
        WHERE type='table' AND name='${ids_table_name}'
        `).get() === undefined
      ) {
        db.prepare(`
          CREATE TABLE '${ids_table_name}' (
            id TEXT PRIMARY KEY,
            path TEXT,
            ast_json TEXT
          )`
        ).run();
      }
      if (
        db.prepare(`SELECT name
        FROM sqlite_master
        WHERE type='table' AND name='${includes_table_name}'
        `).get() === undefined
      ) {
        db.prepare(`
          CREATE TABLE '${includes_table_name}' (
            from_id TEXT,
            from_path TEXT,
            to_id TEXT
          )`
        ).run();
      }
      if (
        db.prepare(`SELECT name
        FROM sqlite_master
        WHERE type='table' AND name='${files_table_name}'
        `).get() === undefined
      ) {
        db.prepare(`
          CREATE TABLE '${files_table_name}' (
            path TEXT PRIMARY KEY,
            toplevel_id TEXT UNIQUE
          )`
        ).run();
      }
      convert_input_options.db = db;
      convert_input_options.ids_table_name = ids_table_name;
      convert_input_options.includes_table_name = includes_table_name;
      convert_input_options.files_table_name = files_table_name;
      convert_input_options.file_provider = new SqliteFileProvider(convert_input_options);
      convert_input_options.id_provider = new SqliteIdProvider(convert_input_options);
    }
    if (commander.generateRedirects) {
      generate_redirects(convert_input_options, cirodown_json, outdir);
      process.exit(0);
    }
    if (commander.watch) {
      if (publish) {
        cli_error('--publish and --watch are incompatible');
      }
      if (!input_path_is_file) {
        convert_files_in_directory_extract_ids(inputPath, options, convert_input_options);
        generate_redirects(convert_input_options, cirodown_json, outdir);
      }
      let watcher = chokidar.watch(inputPath, {ignored: DEFAULT_IGNORE_BASENAMES});
      watcher.on('change', function(path) {
        convert_path_to_file(input_dir, path, options, convert_input_options);
      }).on('add', function(path) {
        convert_path_to_file(input_dir, path, options, convert_input_options);
      });
    } else {
      if (input_path_is_file) {
        if (publish) {
          cli_error('--publish must take a directory as input, not a file');
        }
        output = convert_path(inputPath, options, convert_input_options);
      } else {
        let actual_input;
        let publish_branch;
        let publish_outdir;
        let remote_url;
        let src_branch;

        if (publish) {
          // Clone the source to ensure that only git tracked changes get built and published.
          if (!fs.existsSync(path.join(inputPath, '.git'))) {
            cli_error('--publish must point to the root of a git repository');
          }
          remote_url = cmd_get_stdout('git', ['-C', inputPath, 'config', '--get', 'remote.origin.url'], cmd_options).slice(0, -1);
          src_branch = cmd_get_stdout('git', ['-C', inputPath, 'rev-parse', '--abbrev-ref', 'HEAD'], cmd_options).slice(0, -1);
          if (commander.dryRun) {
            remote_url = 'git@github.com:cirosantilli/cirodown.git';
            src_branch = 'master';
          }
          const parsed_remote_url = git_url_parse(remote_url);
          if (parsed_remote_url.source !== 'github.com') {
            cli_error('only know how  to publish to origin == github.com currently, please send a patch');
          }
          let remote_url_path_components = parsed_remote_url.pathname.split(path.sep);
          if (remote_url_path_components[2].startsWith(remote_url_path_components[1] + '.github.io')) {
            publish_branch = 'master';
          } else {
            publish_branch = 'gh-pages';
          }
          if (src_branch === publish_branch) {
            cli_error(`source and publish branches are the same: ${publish_branch}`);
          }
          fs.mkdirSync(publish_dir, {recursive: true});
          if (commander.publishCommit !== undefined) {
            cmd_get_stdout('git', ['-C', inputPath, 'add', '-u'], cmd_options);
            cmd_get_stdout('git', ['-C', inputPath, 'commit', '-m', commander.publishCommit], cmd_options);
          }
          if (fs.existsSync(publish_git_dir)) {
            cmd_get_stdout('git', ['-C', publish_dir, 'checkout', '--', '.'], cmd_options);
            cmd_get_stdout('git', ['-C', publish_dir, 'pull'], cmd_options);
            cmd_get_stdout('git', ['-C', publish_dir, 'submodule', 'update', '--init'], cmd_options);
          } else {
            cmd_get_stdout('git', ['clone', '--recursive', inputPath, publish_dir], cmd_options);
          }

          // Set some variables especially for publishing.
          actual_input = publish_dir;
          publish_outdir = path.join(publish_out_dir, 'publish');
          convert_input_options.out_css_path = path.join(publish_outdir, cirodown_nodejs.PACKAGE_OUT_CSS_BASENAME);
          convert_input_options.out_js_path = path.join(publish_outdir, cirodown_nodejs.PACKAGE_OUT_JS_BASENAME);
          convert_input_options.external_css_and_js = true;
          // Remove all files from the gh-pages repository in case some were removed from the original source.
          if (fs.existsSync(path.join(publish_out_dir, '.git'))) {
            cmd_get_stdout('git', ['-C', publish_outdir, 'rm', '-r', '-f', '.'], cmd_options);
          }
        } else {
          actual_input = inputPath;
          publish_outdir = outdir;
        }
        convert_input_options.outdir = publish_outdir;

        // Do the actual conversion.
        convert_files_in_directory_extract_ids(actual_input, options, convert_input_options);
        convert_files_in_directory(actual_input, options, convert_input_options);
        generate_redirects(convert_input_options, cirodown_json, publish_outdir);

        // Publish the converted output if build succeeded.
        if (publish && !convert_input_options.had_error) {
          // Push the original source.
          cmd_get_stdout('git', ['-C', inputPath, 'push'], cmd_options);
          cmd_get_stdout('git', ['-C', publish_outdir, 'init'], cmd_options);
          // https://stackoverflow.com/questions/42871542/how-to-create-a-git-repository-with-the-default-branch-name-other-than-master
          cmd_get_stdout('git', ['-C', publish_outdir, 'checkout', '-B', publish_branch], cmd_options);
          try {
            // Fails if remote already exists.
            cmd_get_stdout('git', ['-C', publish_outdir, 'remote', 'add', 'origin', remote_url], cmd_options);
          } catch(error) {
            cmd_get_stdout('git', ['-C', publish_outdir, 'remote', 'set-url', 'origin', remote_url], cmd_options);
          }
          // Ensure that we are up-to-date with the upstream gh-pages if one exists.
          // TODO may fail on initial one, do a check for that.
          cmd_get_stdout('git', ['-C', publish_outdir, 'fetch', 'origin'], cmd_options);
          cmd_get_stdout('git', ['-C', publish_outdir, 'reset', `origin/${publish_branch}`], cmd_options);

          // Generate special files needed for GitHub pages.
          gemfile_content = "gem 'github-pages', group: :jekyll_plugins\n";
          fs.writeFileSync(path.join(publish_outdir, 'Gemfile'), gemfile_content);

          // Commit and push.
          if ('prepublish' in cirodown_json) {
            const prepublish_path = cirodown_json.prepublish
            if (!fs.existsSync(prepublish_path)) {
              cli_error(`${CIRODOWN_JSON_BASENAME} prepublish file not found: ${prepublish_path}`);
            }
            try {
              cmd_get_stdout(path.resolve(prepublish_path), [publish_outdir]);
            } catch(error) {
              cli_error(`${CIRODOWN_JSON_BASENAME} prepublish command exited non-zero, aborting`);
            }
          }
          // Copy the CSS into the repository.
          fs.copyFileSync(cirodown_nodejs.PACKAGE_OUT_CSS_PATH, convert_input_options.out_css_path);
          fs.copyFileSync(cirodown_nodejs.PACKAGE_OUT_JS_LOCAL_PATH, convert_input_options.out_js_path);
          cmd_get_stdout('git', ['-C', publish_outdir, 'add', '.'], cmd_options);
          source_commit = git_sha(inputPath, src_branch);
          cmd_get_stdout('git', ['-C', publish_outdir, 'commit', '-m', source_commit], cmd_options);
          cmd_get_stdout('git', ['-C', publish_outdir, 'push', 'origin', `${publish_branch}:${publish_branch}`], cmd_options);
        }
      }
    }
  }
  if (
    inputPath === undefined ||
    (output !== undefined && commander.stdout)
  ) {
    process.stdout.write(output);
  }
  if (!commander.watch) {
    process.exit(convert_input_options.had_error);
  }
}
